/*
 * Copyright (c) 2023-2024 elsfs Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.elsfs.cloud.common.core.jackson.deser;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.datatype.jsr310.deser.JSR310DateTimeDeserializerBase;
import java.io.IOException;
import java.io.Serial;
import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

/**
 * Deserializer
 *
 * @author hatterjiang
 */
public class ZonedDateTimeDeserializer extends JSR310DateTimeDeserializerBase<ZonedDateTime> {
  @Serial private static final long serialVersionUID = 1L;
  private static final DateTimeFormatter DEFAULT_FORMATTER;
  public static final ZonedDateTimeDeserializer INSTANCE;
  private ZoneId defaultZone = ZoneId.systemDefault();

  /** Default instance, which uses a custom formatter that includes milliseconds. */
  protected ZonedDateTimeDeserializer() {
    this(DEFAULT_FORMATTER, ZoneId.systemDefault());
  }

  /**
   * Default instance, which uses a custom formatter that includes milliseconds.
   *
   * @param formatter used to parse the string.
   * @param defaultZone defaultZone
   */
  public ZonedDateTimeDeserializer(DateTimeFormatter formatter, ZoneId defaultZone) {
    super(ZonedDateTime.class, formatter);
    this.defaultZone = defaultZone;
  }

  /**
   * Constructor used by the default implementation:
   *
   * @param base this value
   * @param leniency this value
   */
  protected ZonedDateTimeDeserializer(ZonedDateTimeDeserializer base, Boolean leniency) {
    this(base._formatter, ZoneId.systemDefault());
  }

  /**
   * Create a copy with different formatter.
   *
   * @param formatter this value
   * @return this value
   */
  protected ZonedDateTimeDeserializer withDateFormat(DateTimeFormatter formatter) {
    return new ZonedDateTimeDeserializer(formatter, ZoneId.systemDefault());
  }

  /**
   * Create a copy with different leniency.
   *
   * @param leniency this value
   * @return this value
   */
  protected ZonedDateTimeDeserializer withLeniency(Boolean leniency) {
    return new ZonedDateTimeDeserializer(this, leniency);
  }

  @Override
  protected ZonedDateTimeDeserializer withShape(JsonFormat.Shape shape) {
    return this;
  }

  @Override
  public ZonedDateTime deserialize(JsonParser parser, DeserializationContext context)
      throws IOException {
    if (parser.hasTokenId(6)) {
      return this._fromString(parser, context, parser.getText());
    } else if (parser.isExpectedStartObjectToken()) {
      return this._fromString(
          parser, context, context.extractScalarFromObject(parser, this, this.handledType()));
    } else {
      if (parser.isExpectedStartArrayToken()) {
        JsonToken t = parser.nextToken();
        if (t == JsonToken.END_ARRAY) {
          return null;
        }

        ZonedDateTime result;
        if ((t == JsonToken.VALUE_STRING || t == JsonToken.VALUE_EMBEDDED_OBJECT)
            && context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
          result = this.deserialize(parser, context);
          if (parser.nextToken() != JsonToken.END_ARRAY) {
            this.handleMissingEndArrayForSingle(parser, context);
          }

          return result;
        }

        if (t == JsonToken.VALUE_NUMBER_INT) {
          int year = parser.getIntValue();
          int month = parser.nextIntValue(-1);
          int day = parser.nextIntValue(-1);
          int hour = parser.nextIntValue(-1);
          int minute = parser.nextIntValue(-1);
          t = parser.nextToken();
          if (t == JsonToken.END_ARRAY) {
            result =
                ZonedDateTime.of(LocalDateTime.of(year, month, day, hour, minute), defaultZone);
          } else {
            int second = parser.getIntValue();
            t = parser.nextToken();
            if (t == JsonToken.END_ARRAY) {
              result =
                  ZonedDateTime.of(
                      LocalDateTime.of(year, month, day, hour, minute, second), defaultZone);
            } else {
              int partialSecond = parser.getIntValue();
              if (partialSecond < 1000
                  && !context.isEnabled(
                      DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)) {
                partialSecond *= 1000000;
              }

              if (parser.nextToken() != JsonToken.END_ARRAY) {
                throw context.wrongTokenException(
                    parser, this.handledType(), JsonToken.END_ARRAY, "Expected array to end");
              }

              result =
                  ZonedDateTime.of(
                      LocalDateTime.of(year, month, day, hour, minute, second, partialSecond),
                      defaultZone);
            }
          }

          return result;
        }

        context.reportInputMismatch(
            this.handledType(),
            "Unexpected token (%s) within Array, expected VALUE_NUMBER_INT",
            new Object[] {t});
      }

      if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
        return (ZonedDateTime) parser.getEmbeddedObject();
      } else {
        if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
          this._throwNoNumericTimestampNeedTimeZone(parser, context);
        }

        return (ZonedDateTime)
            this._handleUnexpectedToken(
                context, parser, "Expected array or string.", new Object[0]);
      }
    }
  }

  protected ZonedDateTime _fromString(JsonParser p, DeserializationContext ctxt, String string0)
      throws IOException {
    String string = string0.trim();
    if (string.isEmpty()) {
      return this._fromEmptyString(p, ctxt, string);
    } else {
      try {
        if (this._formatter == DEFAULT_FORMATTER
            && string.length() > 10
            && string.charAt(10) == 'T'
            && string.endsWith("Z")) {
          if (this.isLenient()) {
            return ZonedDateTime.parse(string.substring(0, string.length() - 1), this._formatter);
          } else {
            JavaType t = this.getValueType(ctxt);
            return (ZonedDateTime)
                ctxt.handleWeirdStringValue(
                    t.getRawClass(),
                    string,
                    "Should not contain offset when 'strict' mode set for property or type (enable"
                        + " 'lenient' handling to allow)",
                    new Object[0]);
          }
        } else {
          return ZonedDateTime.parse(string, this._formatter.withZone(defaultZone));
        }
      } catch (DateTimeException var6) {
        DateTimeException e = var6;
        return this._handleDateTimeException(ctxt, e, string);
      }
    }
  }

  static {
    DEFAULT_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
    INSTANCE = new ZonedDateTimeDeserializer();
  }
}
